package ar.edu.itba.pod.legajo46470.simulation;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.rmi.RemoteException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.Duration;

import ar.edu.itba.balance.api.AgentsBalancer;
import ar.edu.itba.balance.api.NodeAgent;
import ar.edu.itba.balance.api.NotCoordinatorException;
import ar.edu.itba.node.Node;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.pod.agent.runner.Agent;
import ar.edu.itba.pod.agent.runner.Simulation;
import ar.edu.itba.pod.legajo46470.cluster.node.ClusterNode;
import ar.edu.itba.pod.legajo46470.communication.ClusterCommunication;
import ar.edu.itba.pod.multithread.AgentThread;
import ar.edu.itba.pod.multithread.EventDispatcher;
import ar.edu.itba.pod.time.TimeMapper;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;


public class DistributedSimulation implements Simulation {
	private final static Logger LOGGER = Logger.getLogger(DistributedSimulation.class);
	private EventDispatcher dispatcher;
	private final TimeMapper timeMapper;
	private ClusterNode clusterNode;
	private ClusterCommunication<AgentsBalancer> clusterComm;
	
	private Thread timeOutThread;
	private List<AgentThread> agents = Lists.newArrayList();
	
	// expected start and end date of the simulation.
	private DateTime start;
	private DateTime end;
	
	private AtomicBoolean running = new AtomicBoolean();
	
	public DistributedSimulation(TimeMapper timeMapper, ClusterNode clusterNode, EventDispatcher dispatcher) {
		checkNotNull(timeMapper, "Time mapper cannot be null");
		checkNotNull(clusterNode, "Cluster Node cannot be null");
		this.timeMapper = timeMapper;
		this.clusterNode = clusterNode;
		this.dispatcher = dispatcher;
		clusterComm = new ClusterCommunication<AgentsBalancer>(clusterNode, Node.AGENTS_BALANCER);
	}

	public EventDispatcher dispatcher() {
		return this.dispatcher;
	}
	
	// hay que cambiar este metodo!
	public void add(Agent agent) {
		checkNotNull(agent, "Agent cannot be null");
		checkArgument(!this.agents.contains(agent), "Cannot add the same agent twice");
		boolean agentSent = false;
		NodeInformation coordinator = clusterNode.getCoordinator();
		NodeAgent nodeAgent = new NodeAgent(clusterNode.getNodeInfo(), agent);
		
		// Until agent not succesfully sent to coordinator with no error, do an election and keep trying!
		while(!agentSent) {
			try {
				AgentsBalancer agentBalancer = (AgentsBalancer) clusterComm.getStub(coordinator);
				agentBalancer.addAgentToCluster(nodeAgent);
				agentSent = true;
			} catch (IllegalStateException e) {
				// on error, coordinator is down, ask again for it
				coordinator = clusterNode.getCoordinator();
			} catch (RemoteException e) {
				// on error, coordinator is down, ask again for it
				coordinator = clusterNode.getCoordinator();
			} catch (NotCoordinatorException e) {
				coordinator = e.getNewCoordinator();
			}
		}
		
		//clusterNode.getAgentBalancer().addAgentToCluster(new NodeAgent(clusterNode.getNodeInfo(), agent));
		//AgentThread thread = new AgentThread(timeMapper, dispatcher(), agent);
		//addAgentThread(thread);
	}
	
	public void addInternal(List<NodeAgent> nodeAgents) {
		for (NodeAgent nodeAgent : nodeAgents) {
			AgentThread thread = new AgentThread(timeMapper, dispatcher(), nodeAgent.agent());
			addAgentThread(thread);
		}
	}

	protected void addAgentThread(AgentThread thread) {
		this.agents.add(thread);
		
		if (running.get()) {
			thread.start();
		}
	}
	
	@Override
	public void remove(Agent agent) {
		checkNotNull(agent, "Agent cannot be null");
		for (AgentThread thread : agents) {
			if (thread.isRunning(agent)) {
				this.agents.remove(thread);
				if (running.get()) {
					thread.finishAgent();
				}
				return;
			}
		}
	}

	@Override
	public void start(final Duration duration) {
		Preconditions.checkState(running.compareAndSet(false, true), "The simulation is already running!");
		
		this.start = DateTime.now();
		this.end = duration.equals(Duration.ZERO) ? null : this.start.plus(duration);

		for (AgentThread agent : agents) {
			agent.start();
		}
		
		this.timeOutThread = new Thread("Simulation-Timeout") {
			@Override
			public void run() {
				try {
					sleep(duration.getMillis());
				} catch (InterruptedException e) {
					// this shouldn't happen
				}
				try {
					DistributedSimulation.this.stop();
				} catch (InterruptedException e) {
					// this shouldn't happen
				}
			}
		};
		timeOutThread.setDaemon(true);
		timeOutThread.start();
		System.out.println("Simulation started!");
	}

	@Override
	public void startAndWait(Duration duration) throws InterruptedException {
		Preconditions.checkArgument(duration.isLongerThan(Duration.ZERO), "Duration can't be 0");
		start(duration);
		timeOutThread.join();
	}

	@Override
	public void stop() throws InterruptedException {
		for (AgentThread agent : this.agents) {
			agent.finishAgent();
		}

		for (AgentThread agent : this.agents) {
			agent.join();
		}
	}
	
	@Override
	public Duration elapsed() {
		return new Duration(start, null);
	}
	
	public boolean started() {
		return start != null;
	}
	@Override
	public Duration remaining() {
		return new Duration(null, end);
	}
	
	@Override
	public int agentsRunning() {
		return agents.size();
	}

	protected TimeMapper getTimeMapper() {
		return timeMapper;
	}
	
	@Override
	public List<Agent> getAgentsRunning() {
		List<Agent> answer = Lists.newArrayList();
		for (AgentThread agentThread : agents) {
			answer.add(agentThread.getAgent());
		}
		return answer;
	}
}
